// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: daniel

#include "coin2/exchange/coinone/api_order/native_private_client.h"

#include "coin2/exchange/base/api_util/url_params_builder.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/coinone/api_util/auth_key.h"
#include "coin2/exchange/order/coinone.pb.h"

constexpr auto EXAMPLE_SPOT_COINONE = R"(
"connection_config": {
  "key_filepath": "/home/ec2-user/config/trade/xunke06/coinone/trade_key.json",
  "rate_limit_info": {
    "spot_coinone": {
      "req_all": {
        "secs": 1,
        "reqs": 10
      }
    }
  }
})";

CoinoneNativePrivateClient::CoinoneNativePrivateClient(
    const MarketExchangeApi& mea,
    const NativeClientConfig& config) {
  mea_ = mea;
  NativeClientConfig cfg;
  cfg.set_rest_host("api.coinone.co.kr");
  cfg.set_rest_host_port(443);
  cfg.MergeFrom(config);
  LOG(INFO) << "[CoinoneNativePrivateClient] config: " << cfg.DebugString();

  auths_.Construct<CoinoneRestAuth, NativeClientConfig>(cfg);

  // rate limit tracker
  using RateConfig = coin2::exchange::base::order::RateConfig;
  coin2::exchange::order::RateLimitInfoSpotCoinone rate_config;
  RateConfig::Get()->GetConfig(mea_, config, &rate_config);
  if (!rate_config.has_req_all()) {
    RateConfig::CheckFail(mea_.String(), EXAMPLE_SPOT_COINONE);
  }
  const auto& req_all = rate_config.req_all();
  LOG(INFO) << "\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
            << "\nOG request rate was set as below:\n"
            << "\nreq_all: [" << req_all.reqs() << " (reqs) / " << req_all.secs() << " (seconds)]"
            << "\n\nOr you can configure customized rate in driver config. For example: "
            << EXAMPLE_SPOT_COINONE
            << "\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~";
  rate_tracker_ = std::make_unique<Base::RateTracker>(req_all.secs() * 1'000'000'000LL);
  rate_tracker_->SetRateLimitInfo(req_all.reqs(), req_all.reqs());
  rate_tracker_->SetRateLimitInfoString(&rate_config);
}

auto CoinoneNativePrivateClient::QueryAccountBalance() -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "[QueryBalance] failed, rate limit hit!";
    return {};
  }
  UrlParamsBuilder builder;
  auto context = GetAuth()->CreateRequestByPostWithSignature(path_query_account_balance_, &builder);
  Push(1, ts, ActionType::QUERY);

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  return HttpContextPtr(context);
}

auto CoinoneNativePrivateClient::QueryLimitOrders(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "[QueryLimitOrders] failed, rate limit hit!";
    return {};
  }
  const auto& native_symbol = params.currency();
  UrlParamsBuilder builder;
  builder.PutPost("currency", native_symbol);
  auto context = GetAuth()->CreateRequestByPostWithSignature(path_query_limit_orders_, &builder);
  context->extra_info = native_symbol;
  Push(1, ts, ActionType::QUERY);

  context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  return HttpContextPtr(context);
}

auto CoinoneNativePrivateClient::QueryCompleteOrders(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "[QueryCompleteOrders] failed, rate limit hit!";
    return {};
  }
  const auto& native_symbol = params.currency();
  UrlParamsBuilder builder;
  builder.PutPost("currency", native_symbol);
  auto context = GetAuth()->CreateRequestByPostWithSignature(path_query_complete_orders_, &builder);
  context->extra_info = native_symbol;
  Push(1, ts, ActionType::QUERY);

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  return HttpContextPtr(context);
}

auto CoinoneNativePrivateClient::QueryOrderInfo(const RestParams& params) -> HttpContextPtr {
  CHECK(false);

  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "[QueryOrderInfo] failed, rate limit hit!";
    return {};
  }
  const auto& native_symbol = params.currency();
  UrlParamsBuilder builder;
  if (params.has_order_id()) {
    builder.PutPost("order_id", params.order_id());
  }
  builder.PutPost("currency", native_symbol);
  auto context = GetAuth()->CreateRequestByPostWithSignature(path_query_order_info_, &builder);
  context->extra_info = native_symbol;
  Push(1, ts, ActionType::QUERY);

  return HttpContextPtr(context);
}

auto CoinoneNativePrivateClient::PlaceOrder(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::PLACE)) {
    LOG(ERROR) << "[PlaceOrder] failed, rate limit hit!";
    return {};
  }
  const auto& native_symbol = params.currency();
  UrlParamsBuilder builder;
  builder.PutPost("currency", native_symbol);
  builder.PutPost("price", params.price());
  builder.PutPost("qty", params.qty());
  const auto side = params.side();
  std::string path_place_order;
  if (side == "bid") {
    path_place_order = path_limit_buy_order_;
  } else if (side == "ask") {
    path_place_order = path_limit_sell_order_;
  } else {
    NOTREACHED() << "Unknown order side: " << side;
    return {};
  }
  auto context = GetAuth()->CreateRequestByPostWithSignature(path_place_order, &builder);
  context->extra_info = native_symbol;
  Push(1, ts, ActionType::PLACE);

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  return HttpContextPtr(context);
}

auto CoinoneNativePrivateClient::CancelOrder(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::CANCEL)) {
    LOG(ERROR) << "[CancelOrder] failed, rate limit hit!";
    return {};
  }
  const auto& native_symbol = params.currency();
  UrlParamsBuilder builder;
  builder.PutPost("order_id", params.order_id());
  builder.PutPost("currency", native_symbol);
  builder.PutPost("price", params.price());
  builder.PutPost("qty", params.qty());
  builder.PutPost("is_ask", params.side() == "ask" ? 1 : 0);
  auto context = GetAuth()->CreateRequestByPostWithSignature(path_cancel_order_, &builder);
  context->extra_info = native_symbol;
  Push(1, ts, ActionType::CANCEL);

  context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
  return HttpContextPtr(context);
}

auto CoinoneNativePrivateClient::PlaceOrderNorm(const OrderExecution& order_exe) -> HttpContextPtr {
  const auto& order_spec = *order_exe.order_spec;
  const auto proc_order_id = order_exe.proc_order_id;
  auto params = GetRestOrderParams(order_spec, proc_order_id);
  return PlaceOrder(params);
}

auto CoinoneNativePrivateClient::CancelOrderNorm(const OrderExecution& order_exe)
    -> HttpContextPtr {
  auto params = GetRestCancelParams(*order_exe.oc);
  return CancelOrder(params);
}

auto CoinoneNativePrivateClient::GetRestOrderParams(
    const OrderSpec& order_spec,
    int64_t proc_order_id) -> RestParams {
  RestParams params;
  params.set_currency(order_spec.product);
  if (order_spec.order_type == OrderType::LIMIT_ORDER) {
    auto product = CoinoneProduct::FromStr(order_spec.product, order_spec.order_created_time);
    auto product_holder = GetProductHolder(product, order_spec.order_created_time);
    auto formatter = product_holder->product_info().order_formatter();
    params.set_price(formatter.FormatPrice(order_spec.price));
    params.set_qty(formatter.FormatQty(order_spec.qty));
  } else {
    NOTREACHED() << "unsupported order type: " << static_cast<int>(order_spec.order_type);
  }
  if (order_spec.order_side == OrderSide::BUY_ORDER) {
    params.set_side("bid");
  } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
    params.set_side("ask");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
  }

  return params;
}

auto CoinoneNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  const auto& order_spec = order_context.order_spec();
  auto product = CoinoneProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto product_holder = GetProductHolder(product, order_spec.order_created_time);
  auto formatter = product_holder->product_info().order_formatter();
  RestParams params;
  params.set_order_id(order_context.exchange_order_id());
  params.set_currency(order_context.native_product());
  params.set_price(formatter.FormatPrice(order_context.order_price()));
  params.set_qty(formatter.FormatQty(order_context.order_qty()));
  const auto order_side = order_context.order_side();
  if (order_side == OrderSide::BUY_ORDER) {
    params.set_side("bid");
  } else if (order_side == OrderSide::SELL_ORDER) {
    params.set_side("ask");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_side);
  }

  return params;
}

std::shared_ptr<AsioHttpContext> CoinoneNativePrivateClient::SubmitOrderExecution(
    const OrderExecutionProto& order) {
  switch (order.type()) {
    case OrderExecutionProto::SUBMIT_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return PlaceOrder(params);
    }
    case OrderExecutionProto::CANCEL_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return CancelOrder(params);
    }
    default: {
      return nullptr;
    }
  }
}

std::shared_ptr<AsioHttpContext> CoinoneNativePrivateClient::SubmitRestQuery(
    const RestQueryProto& query) {
  RestParams params;
  switch (query.type()) {
    case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
      return QueryAccountBalance();
    }
    case RestQueryProto::QUERY_OPEN_ORDERS: {
      return QueryLimitOrders(params);
    }
    default: {
      return nullptr;
    }
  }
}
